package model;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import model.turtle.Commandable;
import model.turtle.Turtle;
import util.Pixmap;


/**
 * Environment holds the list of Commandables, and all the
 * information for this particular workspace environment.
 *
 * @author Erick Gonzalez
 * @author Angelica Schwartz
 */
public class Environment extends Observable {

    public static final String MOVE = "move";
    public static final String ADD_OBSERVER = "addObserver";
    public static final String ROTATE = "rotate";
    public static final String SET_HEADING = "setHeading";
    public static final String SET_POSITION = "setPosition";
    public static final String SET_TOWARDS = "setTowards";
    public static final String SET_SHAPE = "setImage";
    public static final String SET_PEN_DESIGN = "setPenDesign";
    public static final String SET_PEN_UP = "setPenUp";
    public static final String SET_PEN_DOWN = "setPenDown";
    public static final String MAKE_VISIBLE = "makeVisible";
    public static final String SET_PEN_SIZE = "setPenSize";
    public static final String MAKE_INVISIBLE = "makeInvisible";
    public static final String GET_VISIBILITY = "getVisibility";
    public static final String CLEAR = "clear";
    public static final String GET_X_COORDINATE = "getXCoordinate";
    public static final String GET_Y_COORDINATE = "getYCoordinate";
    public static final String GET_HEADING = "getHeading";
    public static final String SET_IMAGE = "setImage";
    public static final String GET_IMAGE = "getImage";
    public static final String SET_PEN_COLOR = "setPenColor";
    public static final String GET_PEN_COLOR = "getPenColor";
    public static final String GET_PEN_STATE = "getPenState";
    public static final String STAMP = "stamp";
    public static final String CLEAR_STAMPS = "clearStamps";
    public static final String PAINT_COMMAND_NAME = "paint";

    /**
     * default ID for the first Commandable added.
     */
    private static final int DEFAULT_ID = 1;
    /**
     * default grid size.
     */
    private static final int DEFAULT_GRID_SIZE = 10;
    /**
     * default color is gray.
     */
    private static final Color DEFAULT_COLOR = Color.GRAY;
    /**
     * path to get the Commandable class.
     */
    private static final String COMMANDABLE_CLASS_PATH =
            "model.turtle.Commandable";
    /**
     * Maps wrapper class names to their associated primitive
     * class name.
     */
    private static Map<Class, Class> ourWrappersToPrimitives;
    {
        ourWrappersToPrimitives = new HashMap<Class, Class>();
        ourWrappersToPrimitives.put(Boolean.class, boolean.class);
        ourWrappersToPrimitives.put(Byte.class, byte.class);
        ourWrappersToPrimitives.put(Character.class, char.class);
        ourWrappersToPrimitives.put(Short.class, short.class);
        ourWrappersToPrimitives.put(Integer.class, int.class);
        ourWrappersToPrimitives.put(Long.class, long.class);
        ourWrappersToPrimitives.put(Float.class, float.class);
        ourWrappersToPrimitives.put(Double.class, double.class);
    }
    /**
     * maps the Commandable ids to the Commandable objects.
     */
    private Map<Integer, Commandable> myCommandableMap;
    /**
     * holds the size of the background.
     */
    private Dimension myBackgroundSize;
    /**
     * holds the background image.
     */
    private Image myBackgroundImage;
    /**
     * holds the background color.
     */
    private Color myBackgroundColor;
    /**
     * Matches the color index to the associated color.
     */
    private Map<Integer, Color> myColorIndices;
    /**
     * Matches the shape index to the associated
     *      Pixmap image.
     */
    private Map<Integer, Pixmap> myShapeIndicies;
    /**
     * highlight state: highlights the active turtles
     *      true if on, false if off.
     */
    private boolean myHighlightState;
    /**
     * grid state - true if on, false if off.
     */
    private boolean myGridState;
    /**
     * size of grid line spaces.
     */
    private int myGridSize;
    /**
     * the Commandable object that is being focused on by
     *      this Environment.
     */
    private int myFocusedCommandable;

    /**
     * Constructor.
     * @param size is the background size
     */
    public Environment (Dimension size) {
        myBackgroundSize = size;
        myBackgroundColor = DEFAULT_COLOR;
        myCommandableMap = new LinkedHashMap<Integer, Commandable>();
        myColorIndices = new HashMap<Integer, Color>();
        myShapeIndicies = new HashMap<Integer, Pixmap>();
        // default grid off
        myGridState = false;
        //default highlight off
        myHighlightState = false;
        myGridSize = DEFAULT_GRID_SIZE;
        addCommandable(DEFAULT_ID);
        setFocusedCommandable(DEFAULT_ID);
    }

    /**
     * Sets the Commandables in the list to active.
     * @param commandableIds is the list of ids to activate
     */
    public void activateCommandables (List<Integer> commandableIds) {
        deactivateAllCommandables();
        for (Integer commandableId : commandableIds) {
            if (myCommandableMap.containsKey(commandableId)) {
                myCommandableMap.get(commandableId).setActiveState(true);
            } 
            else {
                addCommandable(commandableId);
            }
        }
        setFocusedCommandable(commandableIds.get(commandableIds.size() - 1));
    }

    /**
     * Activates the Commandables with ids divisible by the modulus.
     * @param modulus is the divisor
     * @param stateSelector is to activate or deactivate
     */
    public void activateCommandables (int modulus, boolean stateSelector) {
        int focusedCommandableID = myFocusedCommandable;
        for (Commandable c : myCommandableMap.values()) {
            int commandableIsActive = c.getID() % modulus == 0
                    ? c.setActiveState(stateSelector)
                    : c.setActiveState(!stateSelector);
            focusedCommandableID = commandableIsActive == 1
                    ? c.getID() : focusedCommandableID;
        }
        setFocusedCommandable(focusedCommandableID);
    }

    /**
     * Adds the Commandable with given id to the Environment.
     * @param commandableId to add
     */
    public void addCommandable (int commandableId) {
        Commandable t =
                new Turtle(Turtle.DEFAULT_ICON_IMAGE, Turtle.DEFAULT_LOCATION,
                           Turtle.DEFAULT_SIZE, commandableId);
        myCommandableMap.put(commandableId, t);
        setChangedAndNotifyObservers();
    }

    /**
     * Changes the background image to the desired image.
     * @param i the new image
     */
    public void changeImage (Image i) {
        myBackgroundImage = i;
    }

    /**
     * Deactivates all the Commandables objects in the
     *      Environment.
     */
    public void deactivateAllCommandables() {
        for (Commandable c : myCommandableMap.values()) {
            c.setActiveState(false);
        }
        setFocusedCommandable(-1);
    }

    /**
     * Gets the Color associated with this color index.
     * @param colorIndex the desired index
     * @return the matching Color for this index
     */
    public Color getColor (int colorIndex) {
        return myColorIndices.get(colorIndex);
    }

    /**
     * Gets the Commandable object that the Environment is
     *      focused on.
     * @return the focused Commandable object
     */
    public Commandable getFocusedCommandable () {
        return myCommandableMap.get(myFocusedCommandable);
    }

    /**
     * Gets the background image for this Environment.
     * @return the background image
     */
    public Image getImage () {
        return myBackgroundImage;
    }

    /**
     * Gets the shape associated with this shape index.
     * @param shapeIndex the desired index
     * @return the matching Shape for this index
     */
    public Pixmap getShape (int shapeIndex) {
        return myShapeIndicies.get(shapeIndex);
    }

    /**
     * Uses reflection to invoke the Commandable method name on each
     *      active Commandable.
     * @param methodName is the Commandable's method name
     * @param args is the arguments for this command
     * @return returns the last Commandable so that the return 
     *      value can be calculated
     */
    public Commandable invokeCommandableMethod (String methodName, Object ... args) {
        Class[] argTypes = new Class[args.length];
        for (int i = 0; i < argTypes.length; ++i) {
            Class c = args[i].getClass();
            argTypes[i] =
                    ourWrappersToPrimitives.containsKey(c)
                    ? ourWrappersToPrimitives.get(c) : c;
        }
        try {
            Class c = Class.forName(COMMANDABLE_CLASS_PATH);
            Method m = c.getMethod(methodName, argTypes);
            Commandable focusedCommandable = null;
            for (Commandable commandable : myCommandableMap.values()) {
                if (commandable.isActive() == 1) {
                    m.invoke(commandable, args);
                }
            }
            setChangedAndNotifyObservers();
            return focusedCommandable;
        }
        catch (ClassNotFoundException e) {
            return null;
        }
        catch (NoSuchMethodException e) {
            return null;
        }
        catch (SecurityException e) {
            return null;
        }
        catch (IllegalArgumentException e) {
            return null;
        }
        catch (IllegalAccessException e) {
            return null;
        }
        catch (InvocationTargetException e) {
            return null;
        }
    }

    /**
     * paints the Environment.
     * @param pen is the Graphics pen
     */
    public void paint (Graphics pen) {
        if (myBackgroundImage != null) {
            pen.drawImage(myBackgroundImage, 0, 0,
                          myBackgroundSize.width, myBackgroundSize.height,
                          null);
        }
        else {
            pen.setColor(myBackgroundColor);
            pen.fillRect(0, 0, myBackgroundSize.width, myBackgroundSize.height);
        }
        paintGrid(pen);
        for (Commandable c : myCommandableMap.values()) {
            c.paint(pen);
            if (myHighlightState) {
                c.paintHighlight(pen);
            }
        }
        setChangedAndNotifyObservers();
    }

    /**
     * paints the grid onto the Environment if
     * grid is on.
     * @param pen is the Graphics pen
     */
    private void paintGrid (Graphics pen) {
        if (myGridState) {
            pen.setColor(Color.BLACK);
            for (int i = 0; i < myBackgroundSize.width; i += myGridSize) {
                pen.drawLine(i, 0, i, myBackgroundSize.height);
            }
            for (int j = 0; j < myBackgroundSize.height; j += myGridSize) {
                pen.drawLine(0, j, myBackgroundSize.width, j);
            }
        }
    }

    /**
     * Sets the background color to the desired color index.
     * @param colorIndex is the desired color index
     */
    public void setBackgroundColor (int colorIndex) {
        myBackgroundColor = getColor(colorIndex);
    }

    /**
     * Sets the Environment to changed and notifies its
     * observers of the change.
     */
    public void setChangedAndNotifyObservers () {
        setChanged();
        notifyObservers();
    }

    /**
     * Sets the color index to match the given Color.
     * @param colorIndex is the desired index
     * @param c is the Color to match the new index
     */
    public void setColorIndex (int colorIndex, Color c) {
        myColorIndices.put(colorIndex, c);
    }

    /**
     * Sets the focused commandable that is displayed on the gui info screen
     *      and is used in all commands.
     * @param id is the focused Commandable's id
     * @return id of the focused commandable
     */
    public int setFocusedCommandable (int id) {
        if (myCommandableMap.containsKey(id)) {
            myFocusedCommandable = id;
        }
        return myFocusedCommandable;
    }

    /**
     * Sets the grid size to the desired size.
     * @param size is the desired grid size
     */
    public void setGridSize (int size) {
        myGridSize = size;
    }

    /**
     * Sets the shape index to match the given Pixmap.
     * @param shapeIndex is the desired index
     * @param shape is the Pixmap to match the new index
     */
    public void setShape (int shapeIndex, Pixmap shape) {
        myShapeIndicies.put(shapeIndex, shape);
    }

    /**
     * Toggles the grid on if it is off, or off if it is on.
     */
    public void toggleGrid () {
        myGridState = !myGridState;
    }

    /**
     * Toggles whether active turtles are highlighted.
     * @return 1 if highlight is on, 0 if off
     */
    public int toggleHighlightState () {
        myHighlightState = !myHighlightState;
        return myHighlightState ? 1 : 0;
    }
}
